/*
Module : FTPTransferDlg.CPP
Purpose: Defines the implementation for an MFC dialog which performs FTP uploads 
         and downloads similiar to the Internet Explorer download dialog
Created: PJN / 03-01-2000
History: PJN / 18-10-2000 1. Added a m_bPromptOverwrite variable 
                          2. Fixed a bug in the way the progress control was being updated
         PJN / 11-06-2001 1. Updated copyright details.
                          2. Now provides Bandwidth throtling support. Thanks to Karim Mribti for
                          this good addition.
                          3. Minor code tidy up thanks to BoundsChecker
                          4. m_bPromptForOverwrite now works correctly for downloads as well as uploads
                          5. Fixed a problem with the progress control not updating correctly
         PJN / 10-10-2001 1. Fixed a problem where old style MFC exception handling was being used
                          instead of C++ standard.
         PJN / 17-10-2001 1. Now includes support for PASV
                          2. Fixed a Unicode issue in CFTPTransferDlg::OnStatusCallBack
                          3. Support for precongfigured or direct settings.
                          4. Support for proxies
         PJN / 02-02-2002 1. Now includes support for FTP resumes via the FTP command "REST". Please
                          note that IE 5 is required to support this. This support is experimental,
                          so your feedback is welcome.
                          2. Updated documentation to include explicit requirements for the class.
         PJN / 20-04-2002 1. Modified the code to correctly support FTP resumes. Feeedback from 
                          jony for reporting this issue.
                          2. Fixed a bug in the call to AfxBeginThread
         PJN / 05-06-2002 1. Renamed some string resources used by the class to maintain naming 
                          consistency.
         PJN / 30-10-2002 1. Fixed a problem with seeking to the correct position when resuming a transfer. 
                          Thanks to Liping Dai for this bug report.
         PJN / 13-06-2003 1. Now supports connection timeouts since wininet doesnt <ggg>.
                          2. Other various tidy ups to the code, such as review of all the TRACE statements
                          and calls to GetLastError
         PJN / 20-11-2006 1. Updated the code to clean compile on VC 2005
                          2. Updated copyright details
                          3. Updated the code to compile correctly for Win64
                          4. Integrated the _FTP_DOWNLOAD_DATA class into the CFTPTransferDlg class
                          5. Optimized member variable construction in CFTPTransferDlg constructor
                          6. Code now uses newer C++ style casts in preference to C style casts
                          7. Refactored the code to produce a new CFTPTransferer class which provides
                          a synchronous interface to Wininet FTP which CFTPTransferDlg now inherits 
                          from. This is in line with the most recent updates in the author's CHttpDownloadDlg
                          class
                          8. Class now handles extra large files (> 4GB)
                          9. Code now correctly handles ERROR_INTERNET_EXTENDED_ERROR errors
         PJN / 10-06-2007 1. Minor bug fix to CFTPTransferDlg::SetTimeLeft to correctly handle 64 bit integer
                          parameter.
         PJN / 18-05-2008 1. Updated copyright details
                          2. Updated the logic in CFTPTransferDlg::OnWininetStatusCallBack to correctly handle 
                          ASCII or Unicode strings. Thanks to Hans Dietrich for prompting this update.

Copyright (c) 2000 - 2008 by PJ Naughter (Web: www.naughter.com, Email: pjna@naughter.com)

All rights reserved.

Copyright / Usage Details:

You are allowed to include the source code in any product (commercial, shareware, freeware or otherwise) 
when your product is released in binary form. You are allowed to modify the source code in any way you want 
except you cannot modify the copyright details at the top of each module. If you want to distribute source 
code with your application, then you are only allowed to distribute versions released by the author. This is 
to maintain a single distribution point for the source code. 

All rights reserved.

*/


/////////////////////////////////  Includes  //////////////////////////////////

#include "stdafx.h"
#include "resource.h"
#include "FTPTransferDlg.h"
#ifndef __AFXPRIV_H__
#pragma message("To avoid this message, please put afxpriv.h in your pre compiled header (normally stdafx.h)")
#include <afxpriv.h>
#endif


///////////////////////////////// Macros / Defines ////////////////////////////

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

const UINT WM_FTPTRANSFER_THREAD_FINISHED = WM_APP + 1;


///////////////////////////////// Implementation //////////////////////////////

IMPLEMENT_DYNAMIC(CFTPTransferDlg, CDialog);

CFTPTransferDlg::CFTPTransferDlg(CWnd* pParent)	: CDialog(CFTPTransferDlg::IDD, pParent),
                                                  m_bSafeToClose(FALSE),
                                                  m_pThread(NULL),
                                                  m_dwLastPercentage(static_cast<DWORD>(-1)),
                                                  m_nLastTotalBytes(0),
                                                  m_dwStartProgressTicks(0),
                                                  m_dwLastProgressTicks(0)
{
}

void CFTPTransferDlg::DoDataExchange(CDataExchange* pDX)
{
  //Let the base class do its thing
	CDialog::DoDataExchange(pDX);
	
	DDX_Control(pDX, IDC_STATUS, m_ctrlStatus);
	DDX_Control(pDX, IDC_TRANSFER_RATE, m_ctrlTransferRate);
	DDX_Control(pDX, IDC_TIMELEFT, m_ctrlTimeLeft);
	DDX_Control(pDX, IDC_PROGRESS1, m_ctrlProgress);
	DDX_Control(pDX, IDC_FILESTATUS, m_ctrlFileStatus);
	DDX_Control(pDX, IDC_ANIMATE1, m_ctrlAnimate);
}

BEGIN_MESSAGE_MAP(CFTPTransferDlg, CDialog)
	ON_WM_DESTROY()
	ON_WM_CLOSE()
  ON_MESSAGE(WM_FTPTRANSFER_THREAD_FINISHED, OnThreadFinished)
END_MESSAGE_MAP()

LRESULT CFTPTransferDlg::OnThreadFinished(WPARAM /*wParam*/, LPARAM /*lParam*/)
{
  //It's now safe to close since the thread has signaled us
  m_bSafeToClose = TRUE;

  //Stop the animation
  m_ctrlAnimate.Stop();

  //If an error occured display the message box
  if (Abort())
    EndDialog(IDCANCEL);
  else if (m_sError.GetLength())
  {
    AfxMessageBox(m_sError);
    EndDialog(IDCANCEL);
  }
  else
    EndDialog(IDOK);

  return 0L;
}

BOOL CFTPTransferDlg::OnInitDialog() 
{
  //Let the parent class do its thing
	CDialog::OnInitDialog();
	
	//Empty out the error string  
	m_sError.Empty();
	
	//Reset the values we use for updating the progress
	m_dwLastPercentage = static_cast<DWORD>(-1);
	m_nLastTotalBytes = 0;

  //Change the caption of the dialog if necessary
  if (!m_bDownload)
  {
    CString sCaption;
    sCaption.LoadString(IDS_FTPTRANSFER_UPLOAD_CAPTION);
    SetWindowText(sCaption);
  }

  //Set the range of the progress control
  m_ctrlProgress.SetRange(0, 100);

  //Setup the animation control
  m_ctrlAnimate.Open(IDR_FTPTRANSFER_ANIMATION);

	//Set the file status text
  CString sFileStatus;
  ASSERT(m_sRemoteFile.GetLength());
  ASSERT(m_sServer.GetLength());
  if (m_bDownload)
    AfxFormatString2(sFileStatus, IDS_FTPTRANSFER_FILESTATUS1, m_sRemoteFile, m_sServer);
  else
    AfxFormatString2(sFileStatus, IDS_FTPTRANSFER_FILESTATUS2, m_sRemoteFile, m_sServer);
  m_ctrlFileStatus.SetWindowText(sFileStatus);
  
  //Start the animation to signify that the download is taking place
  m_ctrlAnimate.Play(0, static_cast<UINT>(-1), static_cast<UINT>(-1));

  //Spin off the background thread which will do the actual file transfer
  m_pThread = AfxBeginThread(_TransferThread, this, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED);
  if (m_pThread == NULL)
  {
    TRACE(_T("CFTPTransferDlg::OnInitDialog, Failed to create transfer thread, dialog is aborting\n"));
    EndDialog(IDCANCEL);
    return TRUE;
  }
  m_pThread->m_bAutoDelete = FALSE;
  m_pThread->ResumeThread();
	
	return TRUE;
}

UINT CFTPTransferDlg::_TransferThread(LPVOID pParam)
{
  //Convert from the SDK world to the C++ world
  CFTPTransferDlg* pDlg = reinterpret_cast<CFTPTransferDlg*>(pParam);
  
  //Validate our parameters
  ASSERT(pDlg);
  ASSERT(pDlg->IsKindOf(RUNTIME_CLASS(CFTPTransferDlg)));
  
  pDlg->TransferThread();
  return 0;
}

void CFTPTransferDlg::SetPercentage(int nPercentage)
{
  //Change the progress control
  m_ctrlProgress.SetPos(nPercentage);

  //Change the caption text
  CString sPercentage;
  sPercentage.Format(_T("%d"), nPercentage);
  CString sCaption;
  AfxFormatString2(sCaption, IDS_FTPTRANSFER_PERCENTAGE, sPercentage, m_sRemoteFile);
  SetWindowText(sCaption);
}

void CFTPTransferDlg::SetTimeLeft(DWORD dwSecondsLeft, ULONGLONG dwBytesRead, ULONGLONG dwFileSize)
{
  CString sCopied;
  if (dwBytesRead < 1024)
  {
    CString sBytes;
    sBytes.Format(_T("%I64u"), dwBytesRead);
    AfxFormatString1(sCopied, IDS_FTPTRANSFER_BYTES, sBytes);
  }
  else if (dwBytesRead < 1048576)
  {
    CString sKiloBytes;
    sKiloBytes.Format(_T("%0.1f"), static_cast<LONGLONG>(dwBytesRead)/1024.0);
    AfxFormatString1(sCopied, IDS_FTPTRANSFER_KILOBYTES, sKiloBytes);
  }
  else
  {
    CString sMegaBytes;
    sMegaBytes.Format(_T("%0.2f"), static_cast<LONGLONG>(dwBytesRead)/1048576.0);
    AfxFormatString1(sCopied, IDS_FTPTRANSFER_MEGABYTES, sMegaBytes);
  }

  CString sTotal;
  if (dwFileSize < 1024)
  {
    CString sBytes;
    sBytes.Format(_T("%I64u"), dwFileSize);
    AfxFormatString1(sTotal, IDS_FTPTRANSFER_BYTES, sBytes);
  }
  else if (dwFileSize < 1048576)
  {
    CString sKiloBytes;
    sKiloBytes.Format(_T("%0.1f"), static_cast<LONGLONG>(dwFileSize)/1024.0);
    AfxFormatString1(sTotal, IDS_FTPTRANSFER_KILOBYTES, sKiloBytes);
  }
  else
  {
    CString sMegaBytes;
    sMegaBytes.Format(_T("%0.2f"), static_cast<LONGLONG>(dwFileSize)/1048576.0);
    AfxFormatString1(sTotal, IDS_FTPTRANSFER_MEGABYTES, sMegaBytes);
  }

  CString sOf;
  AfxFormatString2(sOf, IDS_FTPTRANSFER_OF, sCopied, sTotal);

  CString sTime;
  if (dwSecondsLeft < 60)
  {
    CString sSeconds;
    sSeconds.Format(_T("%u"), dwSecondsLeft);
    AfxFormatString1(sTime, IDS_FTPTRANSFER_SECONDS, sSeconds);
  }
  else
  {
    DWORD dwMinutes = dwSecondsLeft / 60;
    DWORD dwSeconds = dwSecondsLeft % 60;
    CString sSeconds;
    sSeconds.Format(_T("%u"), dwSeconds);
    CString sMinutes;
    sMinutes.Format(_T("%u"), dwMinutes);
    if (dwSeconds == 0)
      AfxFormatString1(sTime, IDS_FTPTRANSFER_MINUTES, sMinutes);
    else
      AfxFormatString2(sTime, IDS_FTPTRANSFER_MINUTES_AND_SECONDS, sMinutes, sSeconds);
  }

  CString sTimeLeft;
  AfxFormatString2(sTimeLeft, IDS_FTPTRANSFER_TIMELEFT, sTime, sOf);
  m_ctrlTimeLeft.SetWindowText(sTimeLeft);
}

void CFTPTransferDlg::SetTransferRate(double KbPerSecond)
{
  CString sRate;
  if (KbPerSecond < 1)
  {
    CString sBytesPerSecond;
    sBytesPerSecond.Format(_T("%0.0f"), KbPerSecond*1024);
    AfxFormatString1(sRate, IDS_FTPTRANSFER_BYTESPERSECOND, sBytesPerSecond);
  }
  else if (KbPerSecond < 10)
  {
    CString sKiloBytesPerSecond;
    sKiloBytesPerSecond.Format(_T("%0.2f"), KbPerSecond);
    AfxFormatString1(sRate, IDS_FTPTRANSFER_KILOBYTESPERSECOND, sKiloBytesPerSecond);
  }
  else
  {
    CString sKiloBytesPerSecond;
    sKiloBytesPerSecond.Format(_T("%0.0f"), KbPerSecond);
    AfxFormatString1(sRate, IDS_FTPTRANSFER_KILOBYTESPERSECOND, sKiloBytesPerSecond);
  }
  m_ctrlTransferRate.SetWindowText(sRate);
}

void CFTPTransferDlg::TransferThread()
{
  //Call the function which does the heavy lifting
  Transfer();

  //We're finished
  PostMessage(WM_FTPTRANSFER_THREAD_FINISHED);
}

void CFTPTransferDlg::OnDestroy() 
{
  //Wait for the worker thread to exit
  if (m_pThread)
  {
    WaitForSingleObject(m_pThread->m_hThread, INFINITE);
    delete m_pThread;
    m_pThread = NULL;
  }

  //Let the parent class do its thing
	CDialog::OnDestroy();
}

void CFTPTransferDlg::OnCancel() 
{
  //Just set the abort flag to TRUE and disable the cancel button
  SetAbort();	
  GetDlgItem(IDCANCEL)->EnableWindow(FALSE);
  CFTPTransferer::SetStatus(IDS_FTPTRANSFER_ABORTING_TRANSFER);
}

void CFTPTransferDlg::OnClose() 
{
  if (m_bSafeToClose)	
	  CDialog::OnClose();
  else
  {
    //Just set the abort flag and disable the cancel button
    SetAbort();
    GetDlgItem(IDCANCEL)->EnableWindow(FALSE);
    CFTPTransferer::SetStatus(IDS_FTPTRANSFER_ABORTING_TRANSFER);
  }
}

void CFTPTransferDlg::SetStatus(LPCTSTR pszStatus)
{
  //For debugging purposes also run CFTPTransferer::SetStatus
  CFTPTransferer::SetStatus(pszStatus);

  //Simply update the statis controls text
  m_ctrlStatus.SetWindowText(pszStatus);
}

void CFTPTransferDlg::HandleError(UINT nID, DWORD dwError)
{
  //For debugging purposes also run CHttpDownloader::HandleError
  CFTPTransferer::HandleError(nID, dwError);

  //Form the error string to report
  CString sError;
  if (dwError == ERROR_INTERNET_EXTENDED_ERROR)
  {
    DWORD dwInetError = 0;
    DWORD dwSize=0;
    ::InternetGetLastResponseInfo(&dwInetError, NULL, &dwSize);
    TCHAR* pszResponse = new TCHAR[dwSize+1];
    ::InternetGetLastResponseInfo(&dwInetError, pszResponse, &dwSize);
    pszResponse[dwSize] = _T('\0');
    sError = pszResponse;
    delete [] pszResponse;
  }
  else
    sError = GetErrorMessage(dwError);
  AfxFormatString1(m_sError, nID, sError);
}

void CFTPTransferDlg::OnProgress(ULONGLONG nTotalBytesRead, BOOL bGotFileSize, ULONGLONG nFileSize)
{
  //For debugging purposes also run CHttpDownloader::OnProgress
  CFTPTransferer::OnProgress(nTotalBytesRead, bGotFileSize, nFileSize);

  if (m_dwLastPercentage == -1) //Is this the first time we have been called for this download
  {
    m_dwStartProgressTicks = GetTickCount();
    m_dwLastProgressTicks = m_dwStartProgressTicks;
  }

  if (bGotFileSize)
  {
    //Update the percentage downloaded in the caption
    DWORD dwPercentage = static_cast<DWORD>(nTotalBytesRead * 100 / nFileSize);
    if (dwPercentage != m_dwLastPercentage)
    {
      //Update the progress control bar
      SetPercentage(dwPercentage);
      m_dwLastPercentage = dwPercentage;
    }
  }

  //Update the transfer rate amd estimated time left every second
  DWORD dwNowTicks = GetTickCount();
  DWORD dwTimeTaken = dwNowTicks - m_dwLastProgressTicks;
  if (dwTimeTaken > 1000)
  {
    double KbPerSecond = (static_cast<double>(static_cast<LONGLONG>(nTotalBytesRead)) - static_cast<double>(static_cast<LONGLONG>(m_nLastTotalBytes))) / (static_cast<double>(dwTimeTaken));
    SetTransferRate(KbPerSecond);

    //Setup for the next time around the loop
    m_dwLastProgressTicks = dwNowTicks;
    m_nLastTotalBytes = nTotalBytesRead;

    if (bGotFileSize)
    {
      //Update the estimated time left if we have downloaded something so far
      if (nTotalBytesRead)
      {
        DWORD dwSecondsLeft = static_cast<DWORD>((static_cast<double>(dwNowTicks) - m_dwStartProgressTicks) / static_cast<double>(static_cast<LONGLONG>(nTotalBytesRead)) * (static_cast<double>(static_cast<LONGLONG>(nFileSize)) - static_cast<double>(static_cast<LONGLONG>(nTotalBytesRead))) / 1000);
        SetTimeLeft(dwSecondsLeft, nTotalBytesRead, nFileSize);
      }
    }
  }
}

void CFTPTransferDlg::OnWininetStatusCallBack(HINTERNET /*hInternet*/, DWORD dwInternetStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength)
{
  //Note the MSDN documentation has very poor information on what the lpStatusInformation parameter actually contains for various callbacks. Some users of 
  //the class has reported that it reports a LPTSTR for string type information, but the author has seen it return a LPSTR even for Unicode builds. As a 
  //defense in depth approach against this uncertainty, lets determine at runtime what type of string we have. Perhaps I will check with MS support to get
  //a definitive answer on this. For more info on this, check out http://groups.google.com/group/microsoft.public.inetsdk.programming.wininet/browse_thread/thread/e75e20aa425cfb16/a569b0026815bc98

  USES_CONVERSION;

  switch (dwInternetStatus)
  {
    case INTERNET_STATUS_RESOLVING_NAME:
    {
      //Determine if it is ASCII or Unicode text
      BOOL bUnicodeText = IsTextUnicode(lpvStatusInformation, dwStatusInformationLength, NULL);
    
      //Just delegate back up to the base class to update the status text
      CFTPTransferer::SetStatus(IDS_FTPTRANSFER_RESOLVING_NAME, bUnicodeText ? W2T(static_cast<LPWSTR>(lpvStatusInformation)) : A2T(static_cast<LPSTR>(lpvStatusInformation)));
      break;
    }
    case INTERNET_STATUS_NAME_RESOLVED:
    {
      //Determine if it is ASCII or Unicode text
      BOOL bUnicodeText = IsTextUnicode(lpvStatusInformation, dwStatusInformationLength, NULL);

      CFTPTransferer::SetStatus(IDS_FTPTRANSFER_RESOLVED_NAME, bUnicodeText ? W2T(static_cast<LPWSTR>(lpvStatusInformation)) : A2T(static_cast<LPSTR>(lpvStatusInformation)));
      break;
    }
    case INTERNET_STATUS_CONNECTING_TO_SERVER:
    {
      //Determine if it is ASCII or Unicode text
      BOOL bUnicodeText = IsTextUnicode(lpvStatusInformation, dwStatusInformationLength, NULL);
      
      //Just delegate back up to the base class to update the status text
      CFTPTransferer::SetStatus(IDS_FTPTRANSFER_CONNECTING, bUnicodeText ? W2T(static_cast<LPWSTR>(lpvStatusInformation)) : A2T(static_cast<LPSTR>(lpvStatusInformation)));
      break;
    }
    case INTERNET_STATUS_CONNECTED_TO_SERVER:
    {
      //Determine if it is ASCII or Unicode text
      BOOL bUnicodeText = IsTextUnicode(lpvStatusInformation, dwStatusInformationLength, NULL);
      
      //Just delegate back up to the base class to update the status text
      CFTPTransferer::SetStatus(IDS_FTPTRANSFER_CONNECTED, bUnicodeText ? W2T(static_cast<LPWSTR>(lpvStatusInformation)) : A2T(static_cast<LPSTR>(lpvStatusInformation)));
      break;
    }
    default:
    {
      break;
    }
  }
}
